這系列教學文的目的是要探索具備非同步功能的框架在底層發生了什麼事, 甚至寫一個簡單的框架出來, 目前我們終於進展到閱讀我們的第一個目標 .Net 。 之所以選擇 .Net 作為開始, 一是因為我 C# 比較熟, 二是因為他們擁有很好的文件和索引, 三是因為他們的封裝邏輯很棒(個人感受), 那我們就一起來看看吧。
https://docs.microsoft.com/zh-tw/dotnet/api/system.threading.tasks.task.whenall?view=net-5.0
怕大家不懂 C# , 這邊說明一下, Task.WhenAll 的功能和 JS 的 Promise.all 差不多。
說白了就是停下來等待一堆非同步工作的執行。
要先包裝出多個非同步工作, 接著把這些工作當參數傳入 WhenAll, 最後 WhenAll 會回傳執行結果。這個方法應該是 C# 使用端在實踐非同步 programming 得常見做法了吧。
.Net reference source
https://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/Task.cs,5955
///
/// Creates a task that will complete when all of the supplied tasks have completed.
///
/// The tasks to wait on for completion.
/// A task that represents the completion of all of the supplied tasks.
///
///
/// If any of the supplied tasks completes in a faulted state, the returned task will also complete in a Faulted state,
/// where its exceptions will contain the aggregation of the set of unwrapped exceptions from each of the supplied tasks.
///
///
/// If none of the supplied tasks faulted but at least one of them was canceled, the returned task will end in the Canceled state.
///
///
/// If none of the tasks faulted and none of the tasks were canceled, the resulting task will end in the RanToCompletion state.
///
///
/// If the supplied array/enumerable contains no tasks, the returned task will immediately transition to a RanToCompletion
/// state before it's returned to the caller.
///
可以知道
我們要讀的是這個 overload public static Task WhenAll(params Task[] tasks)
public static Task WhenAll(params Task[] tasks)
{
// Do some argument checking and make a defensive copy of the tasks array
if (tasks == null) throw new ArgumentNullException("tasks");
Contract.EndContractBlock();
int taskCount = tasks.Length;
if (taskCount == 0) return InternalWhenAll(tasks); // Small optimization in the case of an empty array.
Task[] tasksCopy = new Task[taskCount];
for (int i = 0; i < taskCount; i++)
{
Task task = tasks[i];
if (task == null) throw new ArgumentException(Environment.GetResourceString("Task_MultiTaskContinuation_NullTask"), "tasks");
tasksCopy[i] = task;
}
// 以上例外處理, 跳過。
// The rest can be delegated to InternalWhenAll()
return InternalWhenAll(tasksCopy);
}
可以發現, 其複製了一份傳入參數, 丟入 InternalWhenAll(tasksCopy)
, 代其回傳。
接著我們讀一下 InternalWhenAll(tasksCopy)
private static Task InternalWhenAll(Task[] tasks)
{
Contract.Requires(tasks != null, "Expected a non-null tasks array");
return (tasks.Length == 0) ? // take shortcut if there are no tasks upon which to wait
Task.CompletedTask :
new WhenAllPromise(tasks);
}
發現他除了例外處理以外, 又往下丟一層, 進入 WhenAllPromise(tasks)
internal WhenAllPromise(Task[] tasks) : base()
{
Contract.Requires(tasks != null, "Expected a non-null task array");
Contract.Requires(tasks.Length > 0, "Expected a non-zero length task array");
if (AsyncCausalityTracer.LoggingOn)
AsyncCausalityTracer.TraceOperationCreation(CausalityTraceLevel.Required, this.Id, "Task.WhenAll", 0);
if (s_asyncDebuggingEnabled)
{
AddToActiveTasks(this);
}
m_tasks = tasks;
// 記下任務總數
m_count = tasks.Length;
foreach (var task in tasks)
{
// 當這個任務被判斷為已經完成, 則走捷徑
if (task.IsCompleted) this.Invoke(task); // short-circuit the completion action, if possible
else task.AddCompletionAction(this); // simple completion action
}
}
一般來說要看一下繼承, 但 base()
是空的, 跳過。
跳過 log 相關, debug 相關, 我們可以發現兩條核心方法, this.Invoke(task)
和 task.AddCompletionAction(this)
兩條, 但從註解可以看出, 走Invoke
是 AddCompletionAction
的捷徑, 我們先讀Invoke
。 這裡的 this 指的是等待所有任務完成的任務, 就是本 method 的調用者。
讀 public void Invoke(Task completedTask)
https://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/Task.cs,6123
較長, 可以直接到 reference 看, 這裡擷取語句
// atomic 的把 task 的數量減一, 如果減完是 0 表示任務全部完成
if (Interlocked.Decrement(refm_count) == 0)
{
// 設定回傳結果, 略
}
我們整理一下 當 whenAll 開始運行, 會先取得傳入的 task 總數, 接著依序對傳入的 task 判斷狀態, 若是判斷為 task 已完成時, 會把 task 總數減1, 當總數被減為 0 時, 會設定回傳結果且回傳。
Interlocked.Decrement 文件
https://docs.microsoft.com/zh-tw/dotnet/api/system.threading.interlocked.decrement?view=net-5.0
今天我們留下了兩個疑問, 要交給明天的我解決。
明天見!